--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit ff624bc428280bd4675dcbef6332e3378422defe
Parents : e741859
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-30T14:24:11-05:00
fix(meshchat): update public key loading logic to support both 64-byte and 128-byte keys; update identity recall method for consistency
Changes
8 files changed, 78 insertions(+), 24 deletions(-)
Diff
diff --git a/meshchatx/meshchat.py b/meshchatx/meshchat.py
index 3ae35ddc..5658c71e 100644
--- a/meshchatx/meshchat.py
+++ b/meshchatx/meshchat.py
@@ -14947,17 +14947,20 @@ class ReticulumMeshChat:
bytes.fromhex(destination_hash_hex)
raw_bytes = bytes.fromhex(public_key_hex)
- public_key_bytes = (
- raw_bytes[:32] if len(raw_bytes) >= 32 else raw_bytes
- )
identity = RNS.Identity(create_keys=False)
- if not identity.load_public_key(public_key_bytes):
- if len(raw_bytes) == 64:
- raise ValueError("Invalid LXMA public key")
- public_key_bytes = raw_bytes
- if not identity.load_public_key(public_key_bytes):
- raise ValueError("Invalid LXMA public key")
+ loaded = False
+ for candidate in (
+ raw_bytes,
+ raw_bytes[:32] if len(raw_bytes) > 32 else None,
+ ):
+ if not candidate:
+ continue
+ if identity.load_public_key(candidate):
+ loaded = True
+ break
+ if not loaded:
+ raise ValueError("Invalid LXMA public key")
remote_identity_hash = identity.hash.hex()
existing_contact = (
@@ -16924,7 +16927,7 @@ class ReticulumMeshChat:
# We cannot replay "old" paths from the app layer — Transport.request_path refreshes discovery.
path_outcome = await self._await_transport_path(destination_hash_bytes)
- destination_identity = RNS.Identity.recall(destination_hash_bytes)
+ destination_identity = self.recall_identity(destination_hash)
if destination_identity is None:
# we have to bail out of sending, since we don't have the identity/path yet
msg = "Could not find path to destination. Try again later."
diff --git a/meshchatx/src/frontend/components/contacts/ContactsPage.vue b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
index 5ada5b88..3bd9c793 100644
--- a/meshchatx/src/frontend/components/contacts/ContactsPage.vue
+++ b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
@@ -826,7 +826,7 @@ export default {
const publicKeyHex = Array.from(binary)
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
.join("");
- if (publicKeyHex.length !== 128) return null;
+ if (publicKeyHex.length !== 64 && publicKeyHex.length !== 128) return null;
return `lxma://${destinationHash}:${publicKeyHex}`;
} catch {
return null;
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 4f269019..7c7a2c04 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -3042,7 +3042,6 @@ export default {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- remote_identity_hash: this.selectedPeer.destination_hash,
lxmf_address: this.selectedPeer.destination_hash,
name: displayName,
}),
diff --git a/scripts/repack-android-pycodec2-wheels.py b/scripts/repack-android-pycodec2-wheels.py
index 21118408..7db04f55 100755
--- a/scripts/repack-android-pycodec2-wheels.py
+++ b/scripts/repack-android-pycodec2-wheels.py
@@ -38,7 +38,11 @@ def _find_libcodec2(vendor_dir: Path, abi_tag: str, dest_dir: Path) -> Path | No
def _urlsafe_sha256_digest(data: bytes) -> str:
- return base64.urlsafe_b64encode(hashlib.sha256(data).digest()).decode("ascii").rstrip("=")
+ return (
+ base64.urlsafe_b64encode(hashlib.sha256(data).digest())
+ .decode("ascii")
+ .rstrip("=")
+ )
def _rewrite_wheel_record(root: Path) -> None:
diff --git a/tests/backend/test_android_codec2.py b/tests/backend/test_android_codec2.py
index 6d74a157..369b6c7a 100644
--- a/tests/backend/test_android_codec2.py
+++ b/tests/backend/test_android_codec2.py
@@ -23,7 +23,9 @@ def test_ensure_codec2_loads_bundled_library(tmp_path):
with (
patch.object(android_codec2, "_is_chaquopy_android", return_value=True),
- patch.object(android_codec2.ctypes, "CDLL", side_effect=[OSError(), None]) as cdll,
+ patch.object(
+ android_codec2.ctypes, "CDLL", side_effect=[OSError(), None]
+ ) as cdll,
patch.object(
android_codec2,
"_libcodec2_candidates",
@@ -45,7 +47,9 @@ def test_repack_script_bundles_libcodec2(tmp_path):
spec.loader.exec_module(repack_mod)
repack_pycodec2_wheel = repack_mod.repack_pycodec2_wheel
- lib_wheel = tmp_path / "chaquopy_libcodec2-1.2.0-0-py3-none-android_24_arm64_v8a.whl"
+ lib_wheel = (
+ tmp_path / "chaquopy_libcodec2-1.2.0-0-py3-none-android_24_arm64_v8a.whl"
+ )
py_wheel = tmp_path / "pycodec2-4.1.1-0-cp311-cp311-android_24_arm64_v8a.whl"
with zipfile.ZipFile(lib_wheel, "w") as zout:
diff --git a/tests/backend/test_interface_discovery_ifac.py b/tests/backend/test_interface_discovery_ifac.py
index c3a70005..0ac931bb 100644
--- a/tests/backend/test_interface_discovery_ifac.py
+++ b/tests/backend/test_interface_discovery_ifac.py
@@ -316,7 +316,9 @@ async def test_discovered_interfaces_filter_works_with_ifac_network_name(temp_di
assert len(data["interfaces"]) == 2
matching = next(i for i in data["interfaces"] if i["name"] == "matching")
- non_matching = next(i for i in data["interfaces"] if i["name"] == "non-matching")
+ non_matching = next(
+ i for i in data["interfaces"] if i["name"] == "non-matching"
+ )
assert matching["is_allowed"] is True
assert non_matching["is_allowed"] is False
assert matching["network_name"] == "kin.earth"
diff --git a/tests/backend/test_meshchat_coverage.py b/tests/backend/test_meshchat_coverage.py
index 78151b93..ad96233c 100644
--- a/tests/backend/test_meshchat_coverage.py
+++ b/tests/backend/test_meshchat_coverage.py
@@ -324,6 +324,48 @@ async def test_lxm_ingest_uri_lxma_adds_contact(mock_app):
assert payload["destination_hash"] == "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+@pytest.mark.asyncio
+async def test_lxm_ingest_uri_lxma_accepts_128_hex_public_key(mock_app):
+ """64-byte RNS public keys must load from full material, not only the first 32 bytes."""
+ mock_app.database.contacts.get_contact_by_identity_hash.return_value = None
+ mock_app.database.contacts.add_contact = MagicMock()
+ mock_app.message_router.ingest_lxm_uri = MagicMock()
+
+ mock_client = MagicMock()
+ mock_client.send_str = MagicMock(return_value=asyncio.sleep(0))
+
+ fake_identity = MagicMock()
+ fake_identity.hash = bytes.fromhex("bb" * 16)
+
+ def load_public_key(key_bytes):
+ return len(key_bytes) == 64
+
+ fake_identity.load_public_key.side_effect = load_public_key
+
+ with (
+ patch(
+ "meshchatx.meshchat.AsyncUtils.run_async",
+ side_effect=lambda coro: asyncio.create_task(coro),
+ ),
+ patch("meshchatx.meshchat.RNS.Identity", return_value=fake_identity),
+ ):
+ await mock_app.on_websocket_data_received(
+ mock_client,
+ {
+ "type": "lxm.ingest_uri",
+ "uri": f"lxma://{'aa' * 16}:{'1' * 128}",
+ },
+ )
+ await asyncio.sleep(0)
+
+ mock_app.database.contacts.add_contact.assert_called_once()
+ payload = json.loads(mock_client.send_str.call_args[0][0])
+ assert payload["status"] == "success"
+ assert payload["ingest_type"] == "lxma_contact"
+ assert fake_identity.load_public_key.call_count >= 1
+ assert len(fake_identity.load_public_key.call_args[0][0]) == 64
+
+
def test_db_upsert_lxmf_message_basic(mock_app):
mock_msg = MagicMock()
mock_msg.hash = b"h" * 16
@@ -582,8 +624,8 @@ async def _run_send(app, destination_hash="aa" * 16, **kwargs):
fake_lxm.fields = {}
fake_lxm.include_ticket = False
+ app.recall_identity = MagicMock(return_value=fake_identity)
with (
- patch("meshchatx.meshchat.RNS.Identity.recall", return_value=fake_identity),
patch("meshchatx.meshchat.RNS.Destination", return_value=fake_destination),
patch("meshchatx.meshchat.LXMF.LXMessage", return_value=fake_lxm),
patch(
diff --git a/tests/backend/test_message_sending_failures.py b/tests/backend/test_message_sending_failures.py
index 33ee3b43..618c8856 100644
--- a/tests/backend/test_message_sending_failures.py
+++ b/tests/backend/test_message_sending_failures.py
@@ -45,12 +45,12 @@ def mock_app():
@pytest.mark.asyncio
async def test_send_message_no_path_identity_recall_fails(mock_app):
destination_hash = "aa" * 16
- with patch("meshchatx.meshchat.RNS.Identity.recall", return_value=None):
- with pytest.raises(Exception, match="Could not find path to destination"):
- await mock_app.send_message(
- destination_hash=destination_hash,
- content="hi",
- )
+ mock_app.recall_identity = MagicMock(return_value=None)
+ with pytest.raises(Exception, match="Could not find path to destination"):
+ await mock_app.send_message(
+ destination_hash=destination_hash,
+ content="hi",
+ )
@pytest.mark.asyncio
@@ -60,8 +60,8 @@ async def test_send_message_immediate_exception_in_router(mock_app):
mock_app.message_router.handle_outbound.side_effect = Exception("Router failure")
+ mock_app.recall_identity = MagicMock(return_value=fake_identity)
with (
- patch("meshchatx.meshchat.RNS.Identity.recall", return_value=fake_identity),
patch("meshchatx.meshchat.RNS.Destination", return_value=MagicMock()),
patch("meshchatx.meshchat.LXMF.LXMessage", return_value=MagicMock()),
):
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────